﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Threading.Tasks;

namespace Nes4Net
{
    /// <summary>
    /// Audio Processing Unit implementation
    /// </summary>
    public class Apu
    {
        private Double CPU_FREQ_NTSC = 1789772.5; //1789772.72727272d;
        // private Double CPU_FREQ_PAL = 1773447.4;

        public Nes nes { get; private set; }
        private ChannelSquare square1;
        private ChannelSquare square2;
        private ChannelTriangle triangle;
        private ChannelNoise noise;
        private ChannelDM dmc;
        private Int32 SampleRate;
        private Int32 SampleTimerMax;
        private object FrameTime;
        private Int32[] Panning;
        private Int32 masterFrameCounter;
        private Int32 derivedFrameCounter;
        private Int32 countSequence;
        private Int32 sampleCount;
        private int triValue;
        private Int32 initCounter;
        private bool frameIrqEnabled;
        private Int32 frameIrqCounterMax;
        private Int32 channelEnableValue;
        private bool startedPlaying;
        private bool recordOutput;
        private int prevSampleL;
        private int prevSampleR;
        private int smpAccumL;
        private int smpAccumR;
        private int dacRange;
        private int dcValue;
        private int maxSample;
        private int minSample;
        private bool initingHardware;
        private Double accCount;
        private Int32 smpSquare1;
        private Int32 smpSquare2;
        private int smpTriangle;
        private int smpDmc;
        private int[] noiseWavelengthLookup;
        private int[] lengthLookup;
        private int[] dmcFreqLookup;
        private int masterVolume;
        private Int32 stereoPosLSquare1;
        private Int32 stereoPosLSquare2;
        private int stereoPosLTriangle;
        private int stereoPosLNoise;
        private int stereoPosLDMC;
        private int stereoPosRSquare1;
        private int stereoPosRSquare2;
        private int stereoPosRTriangle;
        private int stereoPosRNoise;
        private int stereoPosRDMC;
        private Int32[] square_table;
        private Int32[] tnd_table;
        private bool frameIrqActive;
        private int frameTime;
        private int extraCycles;
        private int sampleTimer;

        public Apu(Nes nes) 
        {
            this.nes = nes;

            this.square1 = new ChannelSquare(this, true);
            this.square2 = new ChannelSquare(this, false);
            this.triangle = new ChannelTriangle(this);
            this.noise = new ChannelNoise(this);
            this.dmc = new ChannelDM(this);

            // TODO XXX
            //this.frameIrqCounter = null;
            this.frameIrqCounterMax = 4;
            this.initCounter = 2048;
            //this.channelEnableValue = null;

            this.SampleRate = 44100;

            this.lengthLookup = null;
            this.dmcFreqLookup = null;
            this.noiseWavelengthLookup = null;
            this.square_table = null;
            this.tnd_table = null;

            this.frameIrqEnabled = false;
            //this.frameIrqActive = null;
            // TODO XXX
            //this.frameClockNow = null;
            this.startedPlaying = false;
            this.recordOutput = false;
            this.initingHardware = false;

            //this.masterFrameCounter = null;
            //this.derivedFrameCounter = null;
            //this.countSequence = null;
            //this.sampleTimer = null;
            //this.frameTime = null;
            //this.sampleTimerMax = null;
            //this.sampleCount = null;
            this.triValue = 0;

            //this.smpSquare1 = null;
            //this.smpSquare2 = null;
            //this.smpTriangle = null;
            //this.smpDmc = null;
            //this.accCount = null;

            // DC removal vars:
            this.prevSampleL = 0;
            this.prevSampleR = 0;
            this.smpAccumL = 0;
            this.smpAccumR = 0;

            // DAC range:
            this.dacRange = 0;
            this.dcValue = 0;

            // Master volume:
            this.masterVolume = 256;

            // Stereo positioning:
            //this.stereoPosLSquare1 = null;
            //this.stereoPosLSquare2 = null;
            //this.stereoPosLTriangle = null;
            //this.stereoPosLNoise = null;
            //this.stereoPosLDMC = null;
            //this.stereoPosRSquare1 = null;
            //this.stereoPosRSquare2 = null;
            //this.stereoPosRTriangle = null;
            //this.stereoPosRNoise = null;
            //this.stereoPosRDMC = null;

            //this.extraCycles = null;

            //this.maxSample = null;
            //this.minSample = null;

            // Panning:
            this.Panning = new Int32[] { 80, 170, 100, 150, 128 };
            this.SetPanning(this.Panning);

            // Initialize lookup tables:
            this.InitLengthLookup();
            this.InitDmcFrequencyLookup();
            this.InitNoiseWavelengthLookup();
            this.InitDACtables();

            // Init sound registers:
            for (var i = 0; i < 0x14; i++)
            {
                if (i == 0x10)
                {
                    this.WriteReg(0x4010, 0x10);
                }
                else
                {
                    this.WriteReg(0x4000 + i, 0);
                }
            }

            this.Reset();
        }

        public void Reset() 
        {
            this.SampleRate = this.nes.Opts.SampleRate;
            this.SampleTimerMax = (Int32)Math.Floor(
              (1024.0 * CPU_FREQ_NTSC * this.nes.Opts.PreferredFrameRate) /
                (this.SampleRate * 60.0)
            );
            this.FrameTime = Math.Floor(
              (14915.0 * this.nes.Opts.PreferredFrameRate) / 60.0
            );

            this.UpdateChannelEnable(0);
            this.masterFrameCounter = 0;
            this.derivedFrameCounter = 0;
            this.countSequence = 0;
            this.sampleCount = 0;
            this.initCounter = 2048;
            this.frameIrqEnabled = false;
            this.initingHardware = false;

            this.ResetCounter();

            this.square1.Reset();
            this.square2.Reset();
            this.triangle.Reset();
            this.noise.reset();
            this.dmc.reset();

            this.accCount = 0;
            this.smpSquare1 = 0;
            this.smpSquare2 = 0;
            this.smpTriangle = 0;
            this.smpDmc = 0;

            this.frameIrqEnabled = false;
            this.frameIrqCounterMax = 4;

            this.channelEnableValue = 0xff;
            this.startedPlaying = false;
            this.prevSampleL = 0;
            this.prevSampleR = 0;
            this.smpAccumL = 0;
            this.smpAccumR = 0;

            this.maxSample = -500000;
            this.minSample = 500000;
        }

        public Int32 ReadReg (Int32 address)
        {
            // Read 0x4015:
            var tmp = 0;
            tmp |= this.square1.GetLengthStatus();
            tmp |= this.square2.GetLengthStatus() << 1;
            tmp |= this.triangle.GetLengthStatus() << 2;
            tmp |= this.noise.getLengthStatus() << 3;
            tmp |= this.dmc.getLengthStatus() << 4;
            tmp |= (this.frameIrqActive && this.frameIrqEnabled ? 1 : 0) << 6;
            tmp |= this.dmc.getIrqStatus() << 7;

            this.frameIrqActive = false;
            this.dmc.irqGenerated = false;

            return tmp & 0xffff;
        }

        public void WriteReg (Int32 address, Int32 value)
        {
            if (address >= 0x4000 && address < 0x4004)
            {
                // Square Wave 1 Control
                this.square1.WriteReg(address, value);
                // console.log("Square Write");
            }
            else if (address >= 0x4004 && address < 0x4008)
            {
                // Square 2 Control
                this.square2.WriteReg(address, value);
            }
            else if (address >= 0x4008 && address < 0x400c)
            {
                // Triangle Control
                this.triangle.WriteReg(address, value);
            }
            else if (address >= 0x400c && address <= 0x400f)
            {
                // Noise Control
                this.noise.writeReg(address, value);
            }
            else if (address == 0x4010)
            {
                // DMC Play mode & DMA frequency
                this.dmc.writeReg(address, value);
            }
            else if (address == 0x4011)
            {
                // DMC Delta Counter
                this.dmc.writeReg(address, value);
            }
            else if (address == 0x4012)
            {
                // DMC Play code starting address
                this.dmc.writeReg(address, value);
            }
            else if (address == 0x4013)
            {
                // DMC Play code length
                this.dmc.writeReg(address, value);
            }
            else if (address == 0x4015)
            {
                // Channel enable
                this.UpdateChannelEnable(value);

                if (value != 0 && this.initCounter > 0)
                {
                    // Start hardware initialization
                    this.initingHardware = true;
                }

                // DMC/IRQ Status
                this.dmc.writeReg(address, value);
            }
            else if (address == 0x4017)
            {
                // Frame counter control
                this.countSequence = (value >> 7) & 1;
                this.masterFrameCounter = 0;
                this.frameIrqActive = false;

                if (((value >> 6) & 0x1) == 0)
                {
                    this.frameIrqEnabled = true;
                }
                else
                {
                    this.frameIrqEnabled = false;
                }

                if (this.countSequence == 0)
                {
                    // NTSC:
                    this.frameIrqCounterMax = 4;
                    this.derivedFrameCounter = 4;
                }
                else
                {
                    // PAL:
                    this.frameIrqCounterMax = 5;
                    this.derivedFrameCounter = 0;
                    this.FrameCounterTick();
                }
            }
        }

        public void ResetCounter()
        {
            if (this.countSequence == 0)
            {
                this.derivedFrameCounter = 4;
            }
            else
            {
                this.derivedFrameCounter = 0;
            }
        }

        // Updates channel enable status.
        // This is done on writes to the
        // channel enable register (0x4015),
        // and when the user enables/disables channels
        // in the GUI.
        public void UpdateChannelEnable(Int32 value)
        {
            this.channelEnableValue = value & 0xffff;
            this.square1.SetEnabled((value & 1) != 0);
            this.square2.SetEnabled((value & 2) != 0);
            this.triangle.SetEnabled((value & 4) != 0);
            this.noise.setEnabled((value & 8) != 0);
            this.dmc.setEnabled((value & 16) != 0);
        }

        // Clocks the frame counter. It should be clocked at
        // twice the cpu speed, so the cycles will be
        // divided by 2 for those counters that are
        // clocked at cpu speed.
        public void clockFrameCounter(Int32 nCycles)
        {
            if (this.initCounter > 0)
            {
                if (this.initingHardware)
                {
                    this.initCounter -= nCycles;
                    if (this.initCounter <= 0)
                    {
                        this.initingHardware = false;
                    }
                    return;
                }
            }

            // Don't process ticks beyond next sampling:
            nCycles += this.extraCycles;
            var maxCycles = this.SampleTimerMax - this.sampleTimer;
            if (nCycles << 10 > maxCycles)
            {
                this.extraCycles = ((nCycles << 10) - maxCycles) >> 10;
                nCycles -= this.extraCycles;
            }
            else
            {
                this.extraCycles = 0;
            }

            var dmc = this.dmc;
            var triangle = this.triangle;
            var square1 = this.square1;
            var square2 = this.square2;
            var noise = this.noise;

            // Clock DMC:
            if (dmc.isEnabled)
            {
                dmc.shiftCounter -= nCycles << 3;
                while (dmc.shiftCounter <= 0 && dmc.dmaFrequency > 0)
                {
                    dmc.shiftCounter += dmc.dmaFrequency;
                    dmc.clockDmc();
                }
            }

            // Clock Triangle channel Prog timer:
            if (triangle.ProgTimerMax > 0)
            {
                triangle.ProgTimerCount -= nCycles;
                while (triangle.ProgTimerCount <= 0)
                {
                    triangle.ProgTimerCount += triangle.ProgTimerMax + 1;
                    if (triangle.LinearCounter > 0 && triangle.LengthCounter > 0)
                    {
                        triangle.TriangleCounter++;
                        triangle.TriangleCounter &= 0x1f;

                        if (triangle.IsEnabled)
                        {
                            if (triangle.TriangleCounter >= 0x10)
                            {
                                // Normal value.
                                triangle.SampleValue = triangle.TriangleCounter & 0xf;
                            }
                            else
                            {
                                // Inverted value.
                                triangle.SampleValue = 0xf - (triangle.TriangleCounter & 0xf);
                            }
                            triangle.SampleValue <<= 4;
                        }
                    }
                }
            }

            // Clock Square channel 1 Prog timer:
            square1.ProgTimerCount -= nCycles;
            if (square1.ProgTimerCount <= 0)
            {
                square1.ProgTimerCount += (square1.ProgTimerMax + 1) << 1;

                square1.SquareCounter++;
                square1.SquareCounter &= 0x7;
                square1.UpdateSampleValue();
            }

            // Clock Square channel 2 Prog timer:
            square2.ProgTimerCount -= nCycles;
            if (square2.ProgTimerCount <= 0)
            {
                square2.ProgTimerCount += (square2.ProgTimerMax + 1) << 1;

                square2.SquareCounter++;
                square2.SquareCounter &= 0x7;
                square2.UpdateSampleValue();
            }

            // Clock noise channel Prog timer:
            var acc_c = nCycles;
            if (noise.progTimerCount - acc_c > 0)
            {
                // Do all cycles at once:
                noise.progTimerCount -= acc_c;
                noise.accCount += acc_c;
                noise.accValue += acc_c * noise.sampleValue;
            }
            else
            {
                // Slow-step:
                while (acc_c-- > 0)
                {
                    if (--noise.progTimerCount <= 0 && noise.progTimerMax > 0)
                    {
                        // Update noise shift register:
                        noise.shiftReg <<= 1;
                        noise.tmp =
                          ((noise.shiftReg << (noise.randomMode == 0 ? 1 : 6)) ^
                            noise.shiftReg) &
                          0x8000;
                        if (noise.tmp != 0)
                        {
                            // Sample value must be 0.
                            noise.shiftReg |= 0x01;
                            noise.randomBit = 0;
                            noise.sampleValue = 0;
                        }
                        else
                        {
                            // Find sample value:
                            noise.randomBit = 1;
                            if (noise.isEnabled && noise.lengthCounter > 0)
                            {
                                noise.sampleValue = noise.masterVolume;
                            }
                            else
                            {
                                noise.sampleValue = 0;
                            }
                        }

                        noise.progTimerCount += noise.progTimerMax;
                    }

                    noise.accValue += noise.sampleValue;
                    noise.accCount++;
                }
            }

            // Frame IRQ handling:
            if (this.frameIrqEnabled && this.frameIrqActive)
            {
                this.nes.Cpu.RequestIrq(this.nes.Cpu.IRQ_NORMAL);
            }

            // Clock frame counter at double CPU speed:
            this.masterFrameCounter += nCycles << 1;
            if (this.masterFrameCounter >= this.frameTime)
            {
                // 240Hz tick:
                this.masterFrameCounter -= this.frameTime;
                this.FrameCounterTick();
            }

            // Accumulate sample value:
            this.AccSample(nCycles);

            // Clock sample timer:
            this.sampleTimer += nCycles << 10;
            if (this.sampleTimer >= this.SampleTimerMax)
            {
                // Sample channels:
                this.Sample();
                this.sampleTimer -= this.SampleTimerMax;
            }
        }

        public void AccSample(Int32 cycles)
        {
            // Special treatment for triangle channel - need to interpolate.
            if (this.triangle.SampleCondition)
            {
                this.triValue = (Int32)Math.Floor(
                  (this.triangle.ProgTimerCount << 4) / (this.triangle.ProgTimerMax + 1) * 1.0
                );
                if (this.triValue > 16)
                {
                    this.triValue = 16;
                }
                if (this.triangle.TriangleCounter >= 16)
                {
                    this.triValue = 16 - this.triValue;
                }

                // Add non-interpolated sample value:
                this.triValue += this.triangle.SampleValue;
            }

            // Now sample normally:
            if (cycles == 2)
            {
                this.smpTriangle += this.triValue << 1;
                this.smpDmc += this.dmc.sample << 1;
                this.smpSquare1 += this.square1.SampleValue << 1;
                this.smpSquare2 += this.square2.SampleValue << 1;
                this.accCount += 2;
            }
            else if (cycles == 4)
            {
                this.smpTriangle += this.triValue << 2;
                this.smpDmc += this.dmc.sample << 2;
                this.smpSquare1 += this.square1.SampleValue << 2;
                this.smpSquare2 += this.square2.SampleValue << 2;
                this.accCount += 4;
            }
            else
            {
                this.smpTriangle += cycles * this.triValue;
                this.smpDmc += cycles * this.dmc.sample;
                this.smpSquare1 += cycles * this.square1.SampleValue;
                this.smpSquare2 += cycles * this.square2.SampleValue;
                this.accCount += cycles;
            }
        }

        public void FrameCounterTick()
        {
            this.derivedFrameCounter++;
            if (this.derivedFrameCounter >= this.frameIrqCounterMax)
            {
                this.derivedFrameCounter = 0;
            }

            if (this.derivedFrameCounter == 1 || this.derivedFrameCounter == 3)
            {
                // Clock length & sweep:
                this.triangle.ClockLengthCounter();
                this.square1.ClockLengthCounter();
                this.square2.ClockLengthCounter();
                this.noise.clockLengthCounter();
                this.square1.ClockSweep();
                this.square2.ClockSweep();
            }

            if (this.derivedFrameCounter >= 0 && this.derivedFrameCounter < 4)
            {
                // Clock linear & decay:
                this.square1.ClockEnvDecay();
                this.square2.ClockEnvDecay();
                this.noise.clockEnvDecay();
                this.triangle.ClockLinearCounter();
            }

            if (this.derivedFrameCounter == 3 && this.countSequence == 0)
            {
                // Enable IRQ:
                this.frameIrqActive = true;
            }

            // End of 240Hz tick
        }

        // Samples the channels, mixes the output together, then writes to buffer.
        public void Sample ()
        {
            Int32 sq_index;
            Int32 tnd_index;

            if (this.accCount > 0)
            {
                this.smpSquare1 <<= 4;
                this.smpSquare1 = (Int32)Math.Floor(this.smpSquare1 / this.accCount);

                this.smpSquare2 <<= 4;
                this.smpSquare2 = (Int32)Math.Floor(this.smpSquare2 / this.accCount);

                this.smpTriangle = (Int32)Math.Floor(this.smpTriangle / this.accCount);

                this.smpDmc <<= 4;
                this.smpDmc = (Int32)Math.Floor(this.smpDmc / this.accCount);

                this.accCount = 0;
            }
            else
            {
                this.smpSquare1 = this.square1.SampleValue << 4;
                this.smpSquare2 = this.square2.SampleValue << 4;
                this.smpTriangle = this.triangle.SampleValue;
                this.smpDmc = this.dmc.sample << 4;
            }

            if(this.noise.accCount == 0)
            {
                this.noise.accCount = 1;
            }
            var smpNoise = (Int32)Math.Floor((this.noise.accValue << 4) / this.noise.accCount * 1.0);
            this.noise.accValue = smpNoise >> 4;
            this.noise.accCount = 1;

            // Stereo sound.

            // Left channel:
            sq_index =
              (this.smpSquare1 * this.stereoPosLSquare1 +
                this.smpSquare2 * this.stereoPosLSquare2) >>
              8;
            tnd_index =
              (3 * this.smpTriangle * this.stereoPosLTriangle +
                (smpNoise << 1) * this.stereoPosLNoise +
                this.smpDmc * this.stereoPosLDMC) >>
              8;
            if (sq_index >= this.square_table.Length)
            {
                sq_index = this.square_table.Length - 1;
            }
            if (tnd_index >= this.tnd_table.Length)
            {
                tnd_index = this.tnd_table.Length - 1;
            }
            var sampleValueL =
              this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue;

            // Right channel:
            sq_index =
              (this.smpSquare1 * this.stereoPosRSquare1 +
                this.smpSquare2 * this.stereoPosRSquare2) >>
              8;
            tnd_index =
              (3 * this.smpTriangle * this.stereoPosRTriangle +
                (smpNoise << 1) * this.stereoPosRNoise +
                this.smpDmc * this.stereoPosRDMC) >>
              8;
            if (sq_index >= this.square_table.Length)
            {
                sq_index = this.square_table.Length - 1;
            }
            if (tnd_index >= this.tnd_table.Length)
            {
                tnd_index = this.tnd_table.Length - 1;
            }
            var sampleValueR =
              this.square_table[sq_index] + this.tnd_table[tnd_index] - this.dcValue;

            // Remove DC from left channel:
            var smpDiffL = sampleValueL - this.prevSampleL;
            this.prevSampleL += smpDiffL;
            this.smpAccumL += smpDiffL - (this.smpAccumL >> 10);
            sampleValueL = this.smpAccumL;

            // Remove DC from right channel:
            var smpDiffR = sampleValueR - this.prevSampleR;
            this.prevSampleR += smpDiffR;
            this.smpAccumR += smpDiffR - (this.smpAccumR >> 10);
            sampleValueR = this.smpAccumR;

            // Write:
            //if (sampleValueL > this.maxSample)
            //{
            //    this.maxSample = sampleValueL;
            //}
            //if (sampleValueL < this.minSample)
            //{
            //    this.minSample = sampleValueL;
            //}

            this.nes.Opts?.OnAudioSample(sampleValueL * 2, sampleValueR * 2);

            // Reset sampled values:
            this.smpSquare1 = 0;
            this.smpSquare2 = 0;
            this.smpTriangle = 0;
            this.smpDmc = 0;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public Int32 GetLengthMax (Int32 value)
        {
            return this.lengthLookup[value >> 3];
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public Int32 getDmcFrequency(Int32 value)
        {
            if (value >= 0 && value < 0x10)
            {
                return this.dmcFreqLookup[value];
            }
            return 0;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public Int32 GetNoiseWaveLength(Int32 value)
        {
            if (value >= 0 && value < 0x10)
            {
                return this.noiseWavelengthLookup[value];
            }
            return 0;
        }

        public void SetPanning(Int32[] pos)
        {
            //for (var i = 0; i < 5; i++)
            //{
            //    this.panning[i] = pos[i];
            //}
            this.UpdateStereoPos();
        }

        public void SetMasterVolume(Int32 value)
        {
            if (value < 0)
            {
                value = 0;
            }
            if (value > 256)
            {
                value = 256;
            }
            this.masterVolume = value;
            this.UpdateStereoPos();
        }

        public void UpdateStereoPos()
        {
            this.stereoPosLSquare1 = (this.Panning[0] * this.masterVolume) >> 8;
            this.stereoPosLSquare2 = (this.Panning[1] * this.masterVolume) >> 8;
            this.stereoPosLTriangle = (this.Panning[2] * this.masterVolume) >> 8;
            this.stereoPosLNoise = (this.Panning[3] * this.masterVolume) >> 8;
            this.stereoPosLDMC = (this.Panning[4] * this.masterVolume) >> 8;

            this.stereoPosRSquare1 = this.masterVolume - this.stereoPosLSquare1;
            this.stereoPosRSquare2 = this.masterVolume - this.stereoPosLSquare2;
            this.stereoPosRTriangle = this.masterVolume - this.stereoPosLTriangle;
            this.stereoPosRNoise = this.masterVolume - this.stereoPosLNoise;
            this.stereoPosRDMC = this.masterVolume - this.stereoPosLDMC;
        }

        public void InitLengthLookup()
        {
            // prettier-ignore
            this.lengthLookup = new Int32[]{
                0x0A, 0xFE,
                    0x14, 0x02,
                    0x28, 0x04,
                    0x50, 0x06,
                    0xA0, 0x08,
                    0x3C, 0x0A,
                    0x0E, 0x0C,
                    0x1A, 0x0E,
                    0x0C, 0x10,
                    0x18, 0x12,
                    0x30, 0x14,
                    0x60, 0x16,
                    0xC0, 0x18,
                    0x48, 0x1A,
                    0x10, 0x1C,
                    0x20, 0x1E
                };
        }

        public void InitDmcFrequencyLookup()
        {
            this.dmcFreqLookup = new Int32[16];

            this.dmcFreqLookup[0x0] = 0xd60;
            this.dmcFreqLookup[0x1] = 0xbe0;
            this.dmcFreqLookup[0x2] = 0xaa0;
            this.dmcFreqLookup[0x3] = 0xa00;
            this.dmcFreqLookup[0x4] = 0x8f0;
            this.dmcFreqLookup[0x5] = 0x7f0;
            this.dmcFreqLookup[0x6] = 0x710;
            this.dmcFreqLookup[0x7] = 0x6b0;
            this.dmcFreqLookup[0x8] = 0x5f0;
            this.dmcFreqLookup[0x9] = 0x500;
            this.dmcFreqLookup[0xa] = 0x470;
            this.dmcFreqLookup[0xb] = 0x400;
            this.dmcFreqLookup[0xc] = 0x350;
            this.dmcFreqLookup[0xd] = 0x2a0;
            this.dmcFreqLookup[0xe] = 0x240;
            this.dmcFreqLookup[0xf] = 0x1b0;
            //for(int i=0;i<16;i++)dmcFreqLookup[i]/=8;
        }

        public void InitNoiseWavelengthLookup()
        {
            this.noiseWavelengthLookup = new Int32[16];

            this.noiseWavelengthLookup[0x0] = 0x004;
            this.noiseWavelengthLookup[0x1] = 0x008;
            this.noiseWavelengthLookup[0x2] = 0x010;
            this.noiseWavelengthLookup[0x3] = 0x020;
            this.noiseWavelengthLookup[0x4] = 0x040;
            this.noiseWavelengthLookup[0x5] = 0x060;
            this.noiseWavelengthLookup[0x6] = 0x080;
            this.noiseWavelengthLookup[0x7] = 0x0a0;
            this.noiseWavelengthLookup[0x8] = 0x0ca;
            this.noiseWavelengthLookup[0x9] = 0x0fe;
            this.noiseWavelengthLookup[0xa] = 0x17c;
            this.noiseWavelengthLookup[0xb] = 0x1fc;
            this.noiseWavelengthLookup[0xc] = 0x2fa;
            this.noiseWavelengthLookup[0xd] = 0x3f8;
            this.noiseWavelengthLookup[0xe] = 0x7f2;
            this.noiseWavelengthLookup[0xf] = 0xfe4;
        }

        public void InitDACtables()
        {
            Double value;
            Int32 i;
            Double ival, max_sqr = 0;
            Double max_tnd = 0;

            this.square_table = new Int32[32 * 16];
            this.tnd_table = new Int32[204 * 16];

            for (i = 0; i < 32 * 16; i++)
            {
                value = 95.52 / (8128.0 / (i / 16.0) + 100.0);
                value *= 0.98411;
                value *= 50000.0;
                ival = (Int32)Math.Floor(value);

                this.square_table[i] = (Int32)ival;
                if (ival > max_sqr)
                {
                    max_sqr = ival;
                }
            }

            for (i = 0; i < 204 * 16; i++)
            {
                value = 163.67 / (24329.0 / (i / 16.0) + 100.0);
                value *= 0.98411;
                value *= 50000.0;
                ival = Math.Floor(value);

                this.tnd_table[i] = (Int32)ival;
                if (ival > max_tnd)
                {
                    max_tnd = ival;
                }
            }

            this.dacRange = (Int32)(max_sqr + max_tnd);
            this.dcValue = this.dacRange / 2;
        }
    }

    public class ChannelSquare 
    {
        private Apu papu;
        private int[] dutyLookup;
        private int[] impLookup;
        private bool sqr1;
        private int lengthCounter;
        private bool updateSweepPeriod;
        private int sweepCounter;
        private int sweepCounterMax;
        private int sweepMode;
        private int sweepShiftAmount;
        private int envDecayRate;
        private int envDecayCounter;
        private int envVolume;
        private int masterVolume;
        private int dutyMode;
        private int vol;
        private bool isEnabled;
        private bool lengthCounterEnable;
        private bool sweepActive;
        private bool sweepCarry;
        private bool envDecayDisable;
        private bool envDecayLoopEnable;
        private bool envReset;

        public ChannelSquare(Apu papu, Boolean square1)
        {
            this.papu = papu;

            this.dutyLookup = new Int32[] {
                 0, 1, 0, 0, 0, 0, 0, 0,
                 0, 1, 1, 0, 0, 0, 0, 0,
                 0, 1, 1, 1, 1, 0, 0, 0,
                 1, 0, 0, 1, 1, 1, 1, 1
            };
            // prettier-ignore
            this.impLookup = new Int32[]{
                   1, -1, 0, 0, 0, 0, 0, 0,
                   1, 0, -1, 0, 0, 0, 0, 0,
                   1, 0, 0, 0, -1, 0, 0, 0,
                  -1, 0, 1, 0, 0, 0, 0, 0
            };

            this.sqr1 = square1;
            //this.isEnabled = null;
            //this.lengthCounterEnable = null;
            //this.sweepActive = null;
            //this.envDecayDisable = null;
            //this.envDecayLoopEnable = null;
            //this.envReset = null;
            //this.sweepCarry = null;
            //this.updateSweepPeriod = null;

            //this.progTimerCount = null;
            //this.progTimerMax = null;
            //this.lengthCounter = null;
            //this.squareCounter = null;
            //this.sweepCounter = null;
            //this.sweepCounterMax = null;
            //this.sweepMode = null;
            //this.sweepShiftAmount = null;
            //this.envDecayRate = null;
            //this.envDecayCounter = null;
            //this.envVolume = null;
            //this.masterVolume = null;
            //this.dutyMode = null;
            //this.sweepResult = null;
            //this.sampleValue = null;
            //this.vol = null;

            this.Reset();
        }

        public int ProgTimerCount { get; internal set; }
        public int SquareCounter { get; internal set; }
        public int ProgTimerMax { get; internal set; }
        public int SampleValue { get; internal set; }

        internal void Reset()
        {
            this.ProgTimerCount = 0;
            this.ProgTimerMax = 0;
            this.lengthCounter = 0;
            this.SquareCounter = 0;
            this.sweepCounter = 0;
            this.sweepCounterMax = 0;
            this.sweepMode = 0;
            this.sweepShiftAmount = 0;
            this.envDecayRate = 0;
            this.envDecayCounter = 0;
            this.envVolume = 0;
            this.masterVolume = 0;
            this.dutyMode = 0;
            this.vol = 0;

            this.isEnabled = false;
            this.lengthCounterEnable = false;
            this.sweepActive = false;
            this.sweepCarry = false;
            this.envDecayDisable = false;
            this.envDecayLoopEnable = false;
        }

        public void ClockLengthCounter()
        {
            if (this.lengthCounterEnable && this.lengthCounter > 0)
            {
                this.lengthCounter--;
                if (this.lengthCounter == 0)
                {
                    this.UpdateSampleValue();
                }
            }
        }

        public void ClockEnvDecay()
        {
            if (this.envReset)
            {
                // Reset envelope:
                this.envReset = false;
                this.envDecayCounter = this.envDecayRate + 1;
                this.envVolume = 0xf;
            }
            else if (--this.envDecayCounter <= 0)
            {
                // Normal handling:
                this.envDecayCounter = this.envDecayRate + 1;
                if (this.envVolume > 0)
                {
                    this.envVolume--;
                }
                else
                {
                    this.envVolume = this.envDecayLoopEnable ? 0xf : 0;
                }
            }

            if (this.envDecayDisable)
            {
                this.masterVolume = this.envDecayRate;
            }
            else
            {
                this.masterVolume = this.envVolume;
            }
            this.UpdateSampleValue();
        }

        public void ClockSweep()
        {
            if (--this.sweepCounter <= 0)
            {
                this.sweepCounter = this.sweepCounterMax + 1;
                if (
                  this.sweepActive &&
                  this.sweepShiftAmount > 0 &&
                  this.ProgTimerMax > 7
                )
                {
                    // Calculate result from shifter:
                    this.sweepCarry = false;
                    if (this.sweepMode == 0)
                    {
                        this.ProgTimerMax += this.ProgTimerMax >> this.sweepShiftAmount;
                        if (this.ProgTimerMax > 4095)
                        {
                            this.ProgTimerMax = 4095;
                            this.sweepCarry = true;
                        }
                    }
                    else
                    {
                        this.ProgTimerMax =
                          this.ProgTimerMax -
                          ((this.ProgTimerMax >> this.sweepShiftAmount) -
                            (this.sqr1 ? 1 : 0));
                    }
                }
            }

            if (this.updateSweepPeriod)
            {
                this.updateSweepPeriod = false;
                this.sweepCounter = this.sweepCounterMax + 1;
            }
        }

        public void UpdateSampleValue()
        {
            if (this.isEnabled && this.lengthCounter > 0 && this.ProgTimerMax > 7)
            {
                if (
                  this.sweepMode == 0 &&
                  this.ProgTimerMax + (this.ProgTimerMax >> this.sweepShiftAmount) > 4095
                )
                {
                    //if (this.sweepCarry) {
                    this.SampleValue = 0;
                }
                else
                {
                    this.SampleValue =
                      this.masterVolume *
                      this.dutyLookup[(this.dutyMode << 3) + this.SquareCounter];
                }
            }
            else
            {
                this.SampleValue = 0;
            }
        }

        public void WriteReg(Int32 address, Int32 value)
        {
            var addrAdd = this.sqr1 ? 0 : 4;
            if (address == 0x4000 + addrAdd)
            {
                // Volume/Envelope decay:
                this.envDecayDisable = (value & 0x10) != 0;
                this.envDecayRate = value & 0xf;
                this.envDecayLoopEnable = (value & 0x20) != 0;
                this.dutyMode = (value >> 6) & 0x3;
                this.lengthCounterEnable = (value & 0x20) == 0;
                if (this.envDecayDisable)
                {
                    this.masterVolume = this.envDecayRate;
                }
                else
                {
                    this.masterVolume = this.envVolume;
                }
                this.UpdateSampleValue();
            }
            else if (address == 0x4001 + addrAdd)
            {
                // Sweep:
                this.sweepActive = (value & 0x80) != 0;
                this.sweepCounterMax = (value >> 4) & 7;
                this.sweepMode = (value >> 3) & 1;
                this.sweepShiftAmount = value & 7;
                this.updateSweepPeriod = true;
            }
            else if (address == 0x4002 + addrAdd)
            {
                // Programmable timer:
                this.ProgTimerMax &= 0x700;
                this.ProgTimerMax |= value;
            }
            else if (address == 0x4003 + addrAdd)
            {
                // Programmable timer, length counter
                this.ProgTimerMax &= 0xff;
                this.ProgTimerMax |= (value & 0x7) << 8;

                if (this.isEnabled)
                {
                    this.lengthCounter = this.papu.GetLengthMax(value & 0xf8);
                }

                this.envReset = true;
            }
        }

        public void SetEnabled(Boolean value)
        {
            this.isEnabled = value;
            if (!value)
            {
                this.lengthCounter = 0;
            }
            this.UpdateSampleValue();
        }

        public Int32 GetLengthStatus()
        {
            return this.lengthCounter == 0 || !this.isEnabled ? 0 : 1;
        }
    }

    public class ChannelTriangle 
    {
        private Apu papu;

        public ChannelTriangle(Apu papu) 
        {
            this.papu = papu;

            //this.IsEnabled = null;
            //this.SampleCondition = null;
            //this.lengthCounterEnable = null;
            //this.lcHalt = null;
            //this.lcControl = null;

            //this.ProgTimerCount = null;
            //this.ProgTimerMax = null;
            //this.TriangleCounter = null;
            //this.LengthCounter = null;
            //this.LinearCounter = null;
            //this.lcLoadValue = null;
            //this.SampleValue = null;
            //this.tmp = null;

            this.Reset();
        }

        public int LengthCounter { get; internal set; }

        private bool lengthCounterEnable;

        public int TriangleCounter { get; internal set; }
        public int SampleValue { get; internal set; }
        public int ProgTimerMax { get; internal set; }
        public int ProgTimerCount { get; internal set; }
        public int LinearCounter { get; internal set; }

        private int lcLoadValue;
        private bool lcHalt;
        private bool lcControl;
        private int tmp;

        public bool IsEnabled { get; internal set; }
        public bool SampleCondition { get; internal set; }

        public void Reset()
        {
            this.ProgTimerCount = 0;
            this.ProgTimerMax = 0;
            this.TriangleCounter = 0;
            this.IsEnabled = false;
            this.SampleCondition = false;
            this.LengthCounter = 0;
            this.lengthCounterEnable = false;
            this.LinearCounter = 0;
            this.lcLoadValue = 0;
            this.lcHalt = true;
            this.lcControl = false;
            this.tmp = 0;
            this.SampleValue = 0xf;
        }

        public void ClockLengthCounter()
        {
            if (this.lengthCounterEnable && this.LengthCounter > 0)
            {
                this.LengthCounter--;
                if (this.LengthCounter == 0)
                {
                    this.UpdateSampleCondition();
                }
            }
        }

        public void ClockLinearCounter()
        {
            if (this.lcHalt)
            {
                // Load:
                this.LinearCounter = this.lcLoadValue;
                this.UpdateSampleCondition();
            }
            else if (this.LinearCounter > 0)
            {
                // Decrement:
                this.LinearCounter--;
                this.UpdateSampleCondition();
            }
            if (!this.lcControl)
            {
                // Clear halt flag:
                this.lcHalt = false;
            }
        }

        public Int32 GetLengthStatus()
        {
            return this.LengthCounter == 0 || !this.IsEnabled ? 0 : 1;
        }

        // eslint-disable-next-line no-unused-vars
        public static Int32 ReadReg(Int32 address) => 0;

        public void WriteReg(Int32 address, Int32 value)
        {
            if (address == 0x4008)
            {
                // New values for linear counter:
                this.lcControl = (value & 0x80) != 0;
                this.lcLoadValue = value & 0x7f;

                // Length counter enable:
                this.lengthCounterEnable = !this.lcControl;
            }
            else if (address == 0x400a)
            {
                // Programmable timer:
                this.ProgTimerMax &= 0x700;
                this.ProgTimerMax |= value;
            }
            else if (address == 0x400b)
            {
                // Programmable timer, length counter
                this.ProgTimerMax &= 0xff;
                this.ProgTimerMax |= (value & 0x07) << 8;
                this.LengthCounter = this.papu.GetLengthMax(value & 0xf8);
                this.lcHalt = true;
            }

            this.UpdateSampleCondition();
        }

        public void ClockProgrammableTimer(Int32 nCycles)
        {
            if (this.ProgTimerMax > 0)
            {
                this.ProgTimerCount += nCycles;
                while (
                  this.ProgTimerMax > 0 &&
                  this.ProgTimerCount >= this.ProgTimerMax
                )
                {
                    this.ProgTimerCount -= this.ProgTimerMax;
                    if (
                      this.IsEnabled &&
                      this.LengthCounter > 0 &&
                      this.LinearCounter > 0
                    )
                    {
                        this.ClockTriangleGenerator();
                    }
                }
            }
        }

        public void ClockTriangleGenerator()
        {
            this.TriangleCounter++;
            this.TriangleCounter &= 0x1f;
        }

        public void SetEnabled(Boolean value)
        {
            this.IsEnabled = value;
            if (!value)
            {
                this.LengthCounter = 0;
            }
            this.UpdateSampleCondition();
        }

        public void UpdateSampleCondition()
        {
            this.SampleCondition =
              this.IsEnabled &&
              this.ProgTimerMax > 7 &&
              this.LinearCounter > 0 &&
              this.LengthCounter > 0;
        }
    }

    public class ChannelDM
    {
        private Apu papu;
        private int MODE_NORMAL;
        private int MODE_LOOP;
        private int MODE_IRQ;
        private bool hasSample;
        public bool irqGenerated;
        private int playLength;
        private int playLengthCounter;
        private int reg4013;
        private int playStartAddress;
        private int playAddress;
        private int reg4012;
        private int playMode;
        public int dmaFrequency;
        private int dmaCounter;
        private int deltaCounter;
        private int dacLsb;
        public int shiftCounter;
        public int sample;
        public bool isEnabled;
        private int data;

        public ChannelDM(Apu papu)
        {
            this.papu = papu;

            this.MODE_NORMAL = 0;
            this.MODE_LOOP = 1;
            this.MODE_IRQ = 2;

            //this.isEnabled = null;
            //this.hasSample = null;
            this.irqGenerated = false;

            //this.playMode = null;
            //this.dmaFrequency = null;
            //this.dmaCounter = null;
            //this.deltaCounter = null;
            //this.playStartAddress = null;
            //this.playAddress = null;
            //this.playLength = null;
            //this.playLengthCounter = null;
            //this.shiftCounter = null;
            //this.reg4012 = null;
            //this.reg4013 = null;
            //this.sample = null;
            //this.dacLsb = null;
            //this.data = null;

            this.reset();
        }

        public void clockDmc()
        {
            // Only alter DAC value if the sample buffer has data:
            if (this.hasSample)
            {
                if ((this.data & 1) == 0)
                {
                    // Decrement delta:
                    if (this.deltaCounter > 0)
                    {
                        this.deltaCounter--;
                    }
                }
                else
                {
                    // Increment delta:
                    if (this.deltaCounter < 63)
                    {
                        this.deltaCounter++;
                    }
                }

                // Update sample value:
                this.sample = this.isEnabled ? (this.deltaCounter << 1) + this.dacLsb : 0;

                // Update shift register:
                this.data >>= 1;
            }

            this.dmaCounter--;
            if (this.dmaCounter <= 0)
            {
                // No more sample bits.
                this.hasSample = false;
                this.endOfSample();
                this.dmaCounter = 8;
            }

            if (this.irqGenerated)
            {
                this.papu.nes.Cpu.RequestIrq(this.papu.nes.Cpu.IRQ_NORMAL);
            }
        }

        public void endOfSample()
        {
            if (this.playLengthCounter == 0 && this.playMode == this.MODE_LOOP)
            {
                // Start from beginning of sample:
                this.playAddress = this.playStartAddress;
                this.playLengthCounter = this.playLength;
            }

            if (this.playLengthCounter > 0)
            {
                // Fetch next sample:
                this.nextSample();

                if (this.playLengthCounter == 0)
                {
                    // Last byte of sample fetched, generate IRQ:
                    if (this.playMode == this.MODE_IRQ)
                    {
                        // Generate IRQ:
                        this.irqGenerated = true;
                    }
                }
            }
        }

        public void nextSample()
        {
            // Fetch byte:
            this.data = this.papu.nes.Mmap.Load(this.playAddress);
            this.papu.nes.Cpu.HaltCycles(4);

            this.playLengthCounter--;
            this.playAddress++;
            if (this.playAddress > 0xffff)
            {
                this.playAddress = 0x8000;
            }

            this.hasSample = true;
        }

        public void writeReg(Int32 address, Int32 value)
        {
            if (address == 0x4010)
            {
                // Play mode, DMA Frequency
                if (value >> 6 == 0)
                {
                    this.playMode = this.MODE_NORMAL;
                }
                else if (((value >> 6) & 1) == 1)
                {
                    this.playMode = this.MODE_LOOP;
                }
                else if (value >> 6 == 2)
                {
                    this.playMode = this.MODE_IRQ;
                }

                if ((value & 0x80) == 0)
                {
                    this.irqGenerated = false;
                }

                this.dmaFrequency = this.papu.getDmcFrequency(value & 0xf);
            }
            else if (address == 0x4011)
            {
                // Delta counter load register:
                this.deltaCounter = (value >> 1) & 63;
                this.dacLsb = value & 1;
                this.sample = (this.deltaCounter << 1) + this.dacLsb; // update sample value
            }
            else if (address == 0x4012)
            {
                // DMA address load register
                this.playStartAddress = (value << 6) | 0x0c000;
                this.playAddress = this.playStartAddress;
                this.reg4012 = value;
            }
            else if (address == 0x4013)
            {
                // Length of play code
                this.playLength = (value << 4) + 1;
                this.playLengthCounter = playLength;
                this.reg4013 = value;
            }
            else if (address == 0x4015)
            {
                // DMC/IRQ Status
                if (((value >> 4) & 1) == 0)
                {
                    // Disable:
                    this.playLengthCounter = 0;
                }
                else
                {
                    // Restart:
                    this.playAddress = this.playStartAddress;
                    this.playLengthCounter = this.playLength;
                }
                this.irqGenerated = false;
            }
        }

        public void setEnabled(Boolean value)
        {
            if (!this.isEnabled && value)
            {
                this.playLengthCounter = this.playLength;
            }
            this.isEnabled = value;
        }

        public Int32 getLengthStatus()
        {
            return this.playLengthCounter == 0 || !this.isEnabled ? 0 : 1;
        }

        public Int32 getIrqStatus()
        {
            return this.irqGenerated ? 1 : 0;
        }

        public void reset()
        {
            this.isEnabled = false;
            this.irqGenerated = false;
            this.playMode = this.MODE_NORMAL;
            this.dmaFrequency = 0;
            this.dmaCounter = 0;
            this.deltaCounter = 0;
            this.playStartAddress = 0;
            this.playAddress = 0;
            this.playLength = 0;
            this.playLengthCounter = 0;
            this.sample = 0;
            this.dacLsb = 0;
            this.shiftCounter = 0;
            this.reg4012 = 0;
            this.reg4013 = 0;
            this.data = 0;
        }
    }

    public class ChannelNoise 
    {
        private Apu papu;
        public int shiftReg;
        public int randomBit;
        public int randomMode;
        public int sampleValue;
        public int tmp;
        public int accValue;
        public int accCount;
        public int progTimerCount;
        public int progTimerMax;
        public bool isEnabled;
        public int lengthCounter;
        private bool lengthCounterEnable;
        private bool envDecayDisable;
        private bool envDecayLoopEnable;
        private bool shiftNow;
        private int envDecayRate;
        private int envDecayCounter;
        private int envVolume;
        public int masterVolume;
        private bool envReset;

        public ChannelNoise(Apu papu)
        {
            this.papu = papu;
            //this.isEnabled = null;
            //this.envDecayDisable = null;
            //this.envDecayLoopEnable = null;
            //this.lengthCounterEnable = null;
            //this.envReset = null;
            //this.shiftNow = null;

            //this.lengthCounter = null;
            //this.progTimerCount = null;
            //this.progTimerMax = null;
            //this.envDecayRate = null;
            //this.envDecayCounter = null;
            //this.envVolume = null;
            //this.masterVolume = null;
            this.shiftReg = 1 << 14;
            //this.randomBit = null;
            //this.randomMode = null;
            //this.sampleValue = null;
            this.accValue = 0;
            this.accCount = 1;
            //this.tmp = null;

            this.reset();
        }

        public void reset()
        {
            this.progTimerCount = 0;
            this.progTimerMax = 0;
            this.isEnabled = false;
            this.lengthCounter = 0;
            this.lengthCounterEnable = false;
            this.envDecayDisable = false;
            this.envDecayLoopEnable = false;
            this.shiftNow = false;
            this.envDecayRate = 0;
            this.envDecayCounter = 0;
            this.envVolume = 0;
            this.masterVolume = 0;
            this.shiftReg = 1;
            this.randomBit = 0;
            this.randomMode = 0;
            this.sampleValue = 0;
            this.tmp = 0;
        }

        public void clockLengthCounter()
        {
            if (this.lengthCounterEnable && this.lengthCounter > 0)
            {
                this.lengthCounter--;
                if (this.lengthCounter == 0)
                {
                    this.updateSampleValue();
                }
            }
        }

        public void clockEnvDecay()
        {
            if (this.envReset)
            {
                // Reset envelope:
                this.envReset = false;
                this.envDecayCounter = this.envDecayRate + 1;
                this.envVolume = 0xf;
            }
            else if (--this.envDecayCounter <= 0)
            {
                // Normal handling:
                this.envDecayCounter = this.envDecayRate + 1;
                if (this.envVolume > 0)
                {
                    this.envVolume--;
                }
                else
                {
                    this.envVolume = this.envDecayLoopEnable ? 0xf : 0;
                }
            }
            if (this.envDecayDisable)
            {
                this.masterVolume = this.envDecayRate;
            }
            else
            {
                this.masterVolume = this.envVolume;
            }
            this.updateSampleValue();
        }

        public void updateSampleValue()
        {
            if (this.isEnabled && this.lengthCounter > 0)
            {
                this.sampleValue = this.randomBit * this.masterVolume;
            }
        }

        public void writeReg(Int32 address, Int32 value)
        {
            if (address == 0x400c)
            {
                // Volume/Envelope decay:
                this.envDecayDisable = (value & 0x10) != 0;
                this.envDecayRate = value & 0xf;
                this.envDecayLoopEnable = (value & 0x20) != 0;
                this.lengthCounterEnable = (value & 0x20) == 0;
                if (this.envDecayDisable)
                {
                    this.masterVolume = this.envDecayRate;
                }
                else
                {
                    this.masterVolume = this.envVolume;
                }
            }
            else if (address == 0x400e)
            {
                // Programmable timer:
                this.progTimerMax = this.papu.GetNoiseWaveLength(value & 0xf);
                this.randomMode = value >> 7;
            }
            else if (address == 0x400f)
            {
                // Length counter
                this.lengthCounter = this.papu.GetLengthMax(value & 248);
                this.envReset = true;
            }
            // Update:
            //updateSampleValue();
        }

        public void setEnabled(Boolean value)
        {
            this.isEnabled = value;
            if (!value)
            {
                this.lengthCounter = 0;
            }
            this.updateSampleValue();
        }

        public Int32 getLengthStatus()
        {
            return this.lengthCounter == 0 || !this.isEnabled ? 0 : 1;
        }
    }

    
}
